﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Utility3D;
using Utility3D.Extensions;

namespace Modeller
{
    public class MyCanvas : Canvas
    {
        private Point? _lastMousePosition;
        private PathGeometry _geometry;
        private DrawingSurface _drawingSurface;
        private IPointRef _hilightPoint;
        private IPointRef _dragPoint;
        private Point? _cursor;
        private IPointRef _selectedPoint;
        private LineStroke _selectedStroke;

        public Action InvalidateCanvasVisuals = () => { };
        public Action RecalculateGeometries = () => { };

        public IEnumerable<DrawingSurface> SurfaceCursors { get; set; }

        public DrawingSurface DrawingSurface
        {
            get { return _drawingSurface; }
            set
            {
                _drawingSurface = value;

                RecalculateGeometries();
                InvalidateCanvasVisuals();
            }
        }

        public void RecalculateGeometry()
        {
            if (_drawingSurface == null) return;

            var figures = _drawingSurface
                .WorldModel
                .Strokes
                .OfType<LineStroke>()
                .Select(stroke => new { Start = stroke.Start.GetOnSurface(_drawingSurface), End = stroke.End.GetOnSurface(_drawingSurface) })
                .Where(a => a.Start != null && a.End != null)
                .Select(a => new {Start = ToWindowPoint(a.Start.Value), End = ToWindowPoint(a.End.Value)})
                .Select(a => new PathFigure(a.Start, new[] {new LineSegment(a.End, true)}, false))
                .ToArray();
            _geometry = new PathGeometry(figures);
        }

        private enum PointStatus
        {
            Normal, Hilighted, Dragging, Selected
        }

        private static readonly Dictionary<PointStatus, Brush> StatusBrush = new Dictionary<PointStatus, Brush>
                                                                         {
                                                                             {PointStatus.Normal, Brushes.Black},
                                                                             {PointStatus.Hilighted, Brushes.Red},
                                                                             {PointStatus.Dragging, Brushes.Red},
                                                                             {PointStatus.Selected, Brushes.Green}
                                                                         };

        private void DrawPoint(DrawingContext dc, Point point, PointStatus pointStatus)
        {
            dc.DrawEllipse(Brushes.White, new Pen(StatusBrush[pointStatus], 1), ToWindowPoint(point), 4, 4);
        }

        protected override void OnRender(DrawingContext dc)
        {
            if (_drawingSurface == null) return;
            dc.PushClip(new RectangleGeometry(new Rect(0, 0, ActualWidth, ActualHeight)));
            dc.DrawGeometry(Brushes.White, new Pen(Brushes.Black, 1), _geometry);

            foreach (var pointRef in _drawingSurface.WorldModel.Points)
            {
                var point = pointRef.GetOnSurface(_drawingSurface);
                if (point == null) continue;
                var pointValue = point.Value;
                var pointStatus = PointStatus.Normal;
                if (pointRef == _hilightPoint)
                    pointStatus = PointStatus.Dragging;
                else if (pointRef == _selectedPoint)
                    pointStatus = PointStatus.Selected;
                DrawPoint(dc, pointValue, pointStatus);
            }

            if (_cursor != null)
                DrawPoint(dc, _cursor.Value, PointStatus.Hilighted);

            foreach (var point in _drawingSurface.WorldModel.GetIntersectionPointsOnSurface(_drawingSurface))
            {
                DrawPoint(dc, point, PointStatus.Normal);
            }

            DrawBorder(dc);

            if (SurfaceCursors != null) DrawSurfaceCursors(dc);
        }

        private void DrawSurfaceCursors(DrawingContext dc)
        {
                var pen = new Pen(Brushes.Gray, 1);
            foreach (var plane in SurfaceCursors.Select(ds => ds.Plane))
            {
                var windowPoints = DrawingSurface
                    .Plane
                    .GetIntersectionPoints(plane)
                    .Select(ToWindowPoint)
                    .ToList();

                if (windowPoints.Count != 2) continue;

                dc.DrawLine(pen, windowPoints.First(), windowPoints.Last());
            }
        }

        private void DrawBorder(DrawingContext dc)
        {
            var pen = new Pen(Brushes.Gray, 1);
            dc.DrawLine(pen, ToWindowPoint(new Point(0, 0)), ToWindowPoint(new Point(_drawingSurface.Width, 0)));
            dc.DrawLine(pen, ToWindowPoint(new Point(_drawingSurface.Width, 0)), ToWindowPoint(new Point(_drawingSurface.Width, _drawingSurface.Height)));
            dc.DrawLine(pen, ToWindowPoint(new Point(_drawingSurface.Width, _drawingSurface.Height)), ToWindowPoint(new Point(0, _drawingSurface.Height)));
            dc.DrawLine(pen, ToWindowPoint(new Point(0, _drawingSurface.Height)), ToWindowPoint(new Point(0, 0)));
        }

        public void ProcessMouseMove(Point windowPoint)
        {
            _lastMousePosition = windowPoint;

            var surfacePoint = FromWindowPoint(windowPoint);

            if (_drawingSurface == null) return;
            _cursor = null;
            _selectedStroke = null;
            if (_dragPoint != null)
            {
                _selectedPoint = _dragPoint;
                _dragPoint.SetOnSurface(_drawingSurface, surfacePoint);
                RecalculateGeometries();
                InvalidateCanvasVisuals();
                return;
            }

            var mouseSelectionDistance = FromWindowScale(10);
            _hilightPoint = _drawingSurface.WorldModel.Points
                .Select(pointRef => new {Point = pointRef.GetOnSurface(_drawingSurface), PointRef = pointRef})
                .Where(a => a.Point != null)
                .Select(a => new {Distance = a.Point.Value.Distance(surfacePoint), a.PointRef})
                .Where(a => a.Distance < mouseSelectionDistance)
                .OrderBy(a => a.Distance)
                .Select(a => a.PointRef)
                .FirstOrDefault();

            if (_hilightPoint == null)
            {
                var info = _drawingSurface.WorldModel.Strokes
                    .OfType<LineStroke>()
                    .Select(stroke => new
                                          {
                                              Stroke = stroke,
                                              NearestPoint = stroke.NearestPointOnSurface(_drawingSurface, surfacePoint),
                                              Start = stroke.Start.GetOnSurface(_drawingSurface),
                                              End = stroke.End.GetOnSurface(_drawingSurface)
                                          })
                    .Where(a => a.Start != null && a.End != null && a.NearestPoint != null)
                    .Select(a => new
                                     {
                                         a.Stroke,
                                         DistanceToCursor = a.NearestPoint.Value.Distance(surfacePoint),
                                         DistanceToStart = a.Start.Value.Distance(a.NearestPoint.Value),
                                         DistanceToEnd = a.End.Value.Distance(a.NearestPoint.Value),
                                         a.NearestPoint
                                     })
                    .Where(a => a.DistanceToEnd > mouseSelectionDistance && a.DistanceToStart > mouseSelectionDistance && a.DistanceToCursor < mouseSelectionDistance)
                    .OrderBy(a => a.DistanceToCursor)
                    .Select(a => new {Point = new Point(a.NearestPoint.Value.X, a.NearestPoint.Value.Y), a.Stroke})
                    .FirstOrDefault();
                if (info != null)
                {
                    _cursor = info.Point;
                    _selectedStroke = info.Stroke;
                }
            }

            InvalidateCanvasVisuals();
        }

        private Point ToWindowPoint(Point point)
        {
            var xScale = ActualWidth/_drawingSurface.Width;
            var yScale = ActualHeight/_drawingSurface.Height;
            var scale = Math.Min(xScale, yScale);
            var xOffset = 0.0;
            var yOffset = 0.0;
            if (xScale < yScale) yOffset = (ActualHeight - _drawingSurface.Height*scale)/2;
            if (yScale < xScale) xOffset = (ActualWidth - _drawingSurface.Width*scale)/2;

            return new Point(point.X*scale + xOffset, ActualHeight - (point.Y*scale + yOffset));
        }

        private double FromWindowScale(double value)
        {
            var xScale = _drawingSurface.Width/ActualWidth;
            var yScale = _drawingSurface.Height/ActualHeight;
            var scale = Math.Max(xScale, yScale);
            return value*scale;
        }

        private Point FromWindowPoint(Point point)
        {
            var xScale = _drawingSurface.Width/ActualWidth;
            var yScale = _drawingSurface.Height/ActualHeight;
            var scale = Math.Max(xScale, yScale);
            var xOffset = 0.0;
            var yOffset = 0.0;
            if (xScale > yScale) yOffset = (ActualHeight - _drawingSurface.Height/scale) / 2;
            if (yScale > xScale) xOffset = (ActualWidth - _drawingSurface.Width/scale) / 2;

            var x = Math.Max(Math.Min((point.X - xOffset)*scale, _drawingSurface.Width), 0);
            var y = Math.Max(Math.Min((ActualHeight - point.Y - yOffset)*scale, _drawingSurface.Height), 0);
            return new Point(x, y);
        }

        public void ProcessMouseDown(Point windowPoint)
        {
            var surfacePoint = FromWindowPoint(windowPoint);

            if (_drawingSurface == null) return;
            //TODO: When it glides the point, it needs to check the DepthSlider, and see if it needs to do it on a new surface.
            //TODO: Refactor as much of this as I can into another class, possibly in Utility3D
            ProcessMouseMove(windowPoint);
            if (_hilightPoint != null)
                _dragPoint = _hilightPoint;
            if (_selectedStroke != null)
                _dragPoint = _selectedStroke.BreakAtPoint(_drawingSurface, surfacePoint);
            if (_dragPoint == null)
            {
                var p1 = _drawingSurface.WorldModel.Collections.AddPoint(_drawingSurface, new Point(surfacePoint.X, surfacePoint.Y));
                var p2 = _drawingSurface.WorldModel.Collections.AddPoint(_drawingSurface, new Point(surfacePoint.X, surfacePoint.Y));
                _drawingSurface.WorldModel.Collections.AddStroke<LineStroke>(p1, p2);

                _dragPoint = p2;
            }
        }

        public void ProcessMouseUp()
        {
            _dragPoint = null;
        }

        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            RecalculateGeometries();
            base.OnRenderSizeChanged(sizeInfo);
        }

        public void ClosePoint()
        {
            if (_selectedPoint == null) return;

            var shapes = _drawingSurface.WorldModel.ShapeFromPointRef(_selectedPoint);
            if (shapes.Count() != 1) return;

            shapes.Single().Close();

            RecalculateGeometries();
            InvalidateCanvasVisuals();
        }

        public void Extend()
        {
            if (_lastMousePosition == null) return;

            var modelPoint = FromWindowPoint(_lastMousePosition.Value);
            var closestPoint = GetClosestPoint(modelPoint);

            if (closestPoint == null) return;

            var pointRef = _drawingSurface.WorldModel.Collections.AddPoint(_drawingSurface, modelPoint);
            _drawingSurface.WorldModel.Collections.AddStroke<LineStroke>(pointRef, closestPoint);
        }

        private IPointRef GetClosestPoint(Point modelPoint)
        {
            return _drawingSurface
                .WorldModel
                .Points
                .Select(pRef => new {PointRef = pRef, Point = pRef.GetOnSurface(_drawingSurface)})
                .Where(a => a.Point != null && a.Point != modelPoint)
                .Select(a => new {a.PointRef, Distance = a.Point.Value.Distance(modelPoint)})
                .OrderBy(a => a.Distance)
                .Select(a => a.PointRef)
                .FirstOrDefault();
        }

        //TODO: This function doesn't actually work! Why?
        public void Join()
        {
            if (_dragPoint == null) return;
            if (_lastMousePosition == null) return;

            var modelPoint = FromWindowPoint(_lastMousePosition.Value);
            var closestPoint = GetClosestPoint(modelPoint);

            if (closestPoint == null) return;

            _drawingSurface.WorldModel.ReplacePointRefWithNewPointRef(_dragPoint, closestPoint);
            _dragPoint = null;
        }
    }
}